﻿using System;
using System.Net;
using System.Threading;
using System.Collections.Generic;
using Sencha.Serialization;

namespace Sencha.Direct.Client
{
	public abstract partial class RpcConnection
	{
		internal static int InternalTestsResponseDelay;
		internal static int InternalTestsReadResponseDelay;

		/// <summary>
		/// Batch of requests
		/// </summary>
		internal sealed class Batch : IAsyncResult
		{
			// Fields set at construction which do change after 
			// operation completes
			private const Int32 c_StatePreparing = 0;
			private const Int32 c_StatePending = 1;
			private const Int32 c_StateCompletedSynchronously = 2;
			private const Int32 c_StateCompletedAsynchronously = 3;
			private Int32 m_CompletedState = c_StatePreparing;

			// Fields set at construction which never change while 
			// operation is pending
			private readonly AsyncCallback m_AsyncCallback;
			private readonly Object m_AsyncState;

			// Field that may or may not get set depending on usage
			private ManualResetEvent m_AsyncWaitHandle;
			// Fields set when operation completes
			private Exception m_exception;
			// 
			private RpcConnection m_client;

			/// <summary>
			/// HTTP request
			/// </summary>
			public WebRequest HttpRequest { get; private set; }
			/// <summary>
			/// HTTP response
			/// </summary>
			public WebResponse HttpResponse { get; private set; }
			/// <summary>
			/// Outgound requests list
			/// </summary>
			public IList<OutgoingRequest> Requests { get; private set; }
			/// <summary>
			/// Incoming responses
			/// </summary>
			public IList<IncomingResponse> Responses { get; private set; }
			/// <summary>
			/// Request execution result
			/// </summary>
			public bool Successful { get { return this.IsCompleted && this.m_exception == null; } }
			/// <summary>
			/// HTTP request completing flag
			/// </summary>
			public bool IsCompleted
			{
				get
				{
					var state = Thread.VolatileRead(ref m_CompletedState);
					return state != c_StatePending && state != c_StatePreparing;
				}
			}
			/// <summary>
			/// HTTP request error
			/// </summary>
			public Exception Error { get { return m_exception; } }
			/// <summary>
			/// HTTP request async state
			/// </summary>
			public object AsyncState { get { return m_AsyncState; } }
			/// <summary>
			/// HTTP request sync handle
			/// </summary>
			public WaitHandle AsyncWaitHandle
			{
				get
				{
					if (m_AsyncWaitHandle == null)
					{
						var done = this.IsCompleted;
						var mre = new ManualResetEvent(done);
						if (Interlocked.CompareExchange(ref m_AsyncWaitHandle, mre, null) != null)
						{
							// Another thread created this object's event; dispose 
							// the event we just created
							mre.Close();
						}
						else
						{
							if (!done && IsCompleted)
							{
								// If the operation wasn't done when we created 
								// the event but now it is done, set the event
								m_AsyncWaitHandle.Set();
							}
						}
					}
					return m_AsyncWaitHandle;
				}
			}
			/// <summary>
			/// HTTP request sync status
			/// </summary>
			public bool CompletedSynchronously
			{
				get
				{
					return Thread.VolatileRead(ref m_CompletedState) == c_StateCompletedSynchronously;
				}
			}

			/// <summary>
			/// Constructor
			/// </summary>
			public Batch(RpcConnection client)
			{
				if (client == null)
					throw new ArgumentNullException("client");

				m_client = client;
			}
			/// <summary>
			/// Constructor
			/// </summary>
			public Batch(RpcConnection client, AsyncCallback callback, object asyncState)
			{
				if (client == null)
					throw new ArgumentNullException("client");

				m_client = client;
				this.m_AsyncState = asyncState;
				this.m_AsyncCallback = callback;
			}

			/// <summary>
			/// Abort outgoing request in current batch
			/// </summary>
			/// <param name="requestResult">Outgound request to abort</param>
			public bool Abort(IAsyncResult requestResult)
			{
				var state = Thread.VolatileRead(ref m_CompletedState);
				if (state != c_StatePending)
				{
					lock (this)
					{
						if (this.Requests == null)
							return false;
						for (int i = 0; i < this.Requests.Count; i++)
						{
							var request = this.Requests[i];
							if (request.AsyncResult != requestResult)
								continue;
							this.Requests.RemoveAt(i);
							return true;
						}
					}
					return false;
				}

				this.HttpRequest.Abort();
				if (this.HttpResponse != null)
					this.HttpResponse.Close();

				state = Thread.VolatileRead(ref m_CompletedState);
				if (state == c_StatePending)
					this.SetAsCompleted(new WebException("Request was aborted by user"), true);

				return true;
			}
			/// <summary>
			/// End async HTTP request
			/// </summary>
			public void EndInvoke()
			{
				// This method assumes that only 1 thread calls EndInvoke 
				// for this object
				if (!IsCompleted)
				{
					// If the operation isn't done, wait for it
					AsyncWaitHandle.WaitOne();
					AsyncWaitHandle.Close();
					m_AsyncWaitHandle = null;  // Allow early GC
				}

				// Operation is done: if an exception occured, throw it
				if (this.Error != null)
				{
					ReflectionExtentions.TryToPreserveStackTrace(this.Error);
					throw this.Error;
				}
			}
			/// <summary>
			/// Add new request to current batch
			/// </summary>
			public IAsyncResult Add(OutgoingRequest request, Type resultType, AsyncCallback callback, object asyncState)
			{
				if (request == null)
					throw new ArgumentNullException("request");

				var state = Thread.VolatileRead(ref m_CompletedState);

				if (state != c_StatePreparing)
					throw new InvalidOperationException("Can't change executing batch");

				request.AsyncResult = new AsyncRequestResult(this, resultType, callback, asyncState);

				lock (this)
				{
					if (this.Requests == null)
						this.Requests = new List<OutgoingRequest>(10);

					this.Requests.Add(request);
				}

				return request.AsyncResult;
			}
			/// <summary>
			/// Execute batch against remote server
			/// </summary>
			/// <param name="sync">Set to true to execute in current thread</param>
			public void Execute(bool sync)
			{
				var state = Interlocked.Exchange(ref m_CompletedState, c_StatePending);
				if (state != c_StatePreparing)
				{
					Interlocked.Exchange(ref m_CompletedState, state); // restore old state
					throw new InvalidOperationException("Can't execute batch in current state");
				}

				// prepare new request
				var request = WebRequest.Create(m_client.RouterUri);
				request.Method = "POST";
				request.Timeout = m_client.RequestTimeout;
				request.AuthenticationLevel = System.Net.Security.AuthenticationLevel.None;
				request.ContentType = "application/json";

				this.HttpRequest = request;
				// write batch to WebRequest
				if (!(request is FileWebRequest))
					this.WriteRequests();
				else
					request.Method = "GET";

				// handle WebRequest response
				if (sync)
					this.GetResponseSync();
				else
					this.GetResponseAsync();
			}

			private void GetResponseAsync()
			{
				this.HttpRequest.BeginGetResponse(this.GetResponseCallback, this);
			}
			private void GetResponseCallback(IAsyncResult result)
			{
				var state = (Batch)result.AsyncState;
				var request = state.HttpRequest;
				try
				{
					// this is for tests
					if (RpcConnection.InternalTestsResponseDelay != 0)
					{
						System.Threading.Thread.Sleep(RpcConnection.InternalTestsResponseDelay);
						throw new WebException("Request timeout");
					}

					this.HttpResponse = request.EndGetResponse(result);
					this.Responses = this.ReadResponses();

					this.BindResponses();

					state.SetAsCompleted(null, false);
				}
				catch (WebException e)
				{
					state.SetAsCompleted(e, false);
				}
			}
			private void GetResponseSync()
			{
				try
				{
					this.HttpResponse = this.HttpRequest.GetResponse();
					this.Responses = this.ReadResponses();

					this.BindResponses();
					this.SetAsCompleted(null, true);
				}
				catch (WebException e)
				{
					this.SetAsCompleted(e, true);
				}
			}

			private IncomingResponse[] ReadResponses()
			{
				if (RpcConnection.InternalTestsReadResponseDelay != 0)
					System.Threading.Thread.Sleep(RpcConnection.InternalTestsReadResponseDelay);

				using (var rs = this.HttpResponse.GetResponseStream())
				{
					var serializer = new JsonSerializer(typeof(IncomingResponse[]));

					var buffer = new byte[this.HttpResponse.ContentLength];
					var readed = 0;
					var position = 0;

					while ((readed = rs.Read(buffer, position, buffer.Length - position)) > 0 && position < buffer.Length) { position += readed; }

					var input = new System.IO.MemoryStream(buffer, false);
#if DEBUG
                    var debug = new System.IO.StreamReader(input).ReadToEnd();
                    input.Position = 0;
#endif
					var responses = (IncomingResponse[])serializer.ReadObject(input);
					return responses;
				}
			}
			private void WriteRequests()
			{
				var serializer = new JsonSerializer(typeof(IList<OutgoingRequest>));
				using (var output = this.HttpRequest.GetRequestStream())
				{
					serializer.WriteObject(output, this.Requests);
					output.Close();
				}
			}
			private void BindResponses()
			{
				for (int i = 0; i < this.Responses.Count; i++)
				{
					var response = this.Responses[i];
					var responseTid = response.TransactionId;
					var request = (OutgoingRequest)null;

					for (int r = 0; r < this.Requests.Count; r++)
					{
						var requestTid = this.Requests[i].TransactionId;
						if (responseTid == requestTid)
							request = this.Requests[i];
					}

					if (request == null)
						continue; // we don't has request for that response, strange


					request.AsyncResult.Error = response.Error != null ? new Exceptions.RemoteInvocationException(response.Error) : null;
					request.AsyncResult.Result = response.Result;
				}
			}
			private void SetAsCompleted(Exception exception, Boolean completedSynchronously)
			{
				// Passing null for exception means no error occurred. 
				// This is the common case
				m_exception = exception;

				// The m_CompletedState field MUST be set prior calling the callback
				Int32 prevState = Interlocked.Exchange(ref m_CompletedState,
				   completedSynchronously ? c_StateCompletedSynchronously :
				   c_StateCompletedAsynchronously);
				if (prevState != c_StatePending)
					throw new InvalidOperationException("You can set a result only once");

				// If a callback method was set, call it
				try
				{
					if (m_AsyncCallback != null) m_AsyncCallback(this);
				}
				catch (ThreadAbortException) { throw; }
				catch { }

				// invoke callbacks on requests
				for (int i = 0; i < this.Requests.Count; i++)
				{
					try
					{
						var request = this.Requests[i];
						if (request.AsyncResult.Callback != null)
							request.AsyncResult.Callback(request.AsyncResult);
					}
					catch (ThreadAbortException) { throw; }
					catch { }
				}

				// If the event exists, set it
				if (m_AsyncWaitHandle != null) m_AsyncWaitHandle.Set();
			}
		}

		/// <summary>
		/// Async result state
		/// </summary>
		internal sealed class AsyncRequestResult : IAsyncResult
		{
			private Batch m_batch;
			private AsyncCallback m_callback;
			private object m_callbackState;

			public AsyncRequestResult(Batch batch, Type resultType, AsyncCallback callback, object asyncState)
			{
				if (batch == null)
					throw new ArgumentNullException("batch");

				m_batch = batch;
				m_callback = callback;
				m_callbackState = asyncState;

				this.ResultType = resultType;
			}

			public Batch Batch { get { return m_batch; } }
			public AsyncCallback Callback { get { return m_callback; } }
			public object AsyncState { get { return m_callbackState; } }
			public WaitHandle AsyncWaitHandle { get { return m_batch.AsyncWaitHandle; } }
			public bool CompletedSynchronously { get { return m_batch.CompletedSynchronously; } }
			public bool IsCompleted { get { return m_batch.IsCompleted; } }
			public Exception Error { get; set; }

			public JsonString Result { get; set; }
			public Type ResultType { get; set; }
		}
	}
}
